﻿/**************************************************************
 *
 * 唯一标识：fef1d60d-069d-42ac-9de3-8353772e1300
 * 命名空间：Sgr.AspNetCore.Middlewares
 * 创建时间：2023/8/10 18:01:41
 * 机器名称：DESKTOP-S0D075D
 * 创建者：antho
 * 电子邮箱：fengqinhua2016@163.com
 * 描述：
 *
 **************************************************************/

using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Sgr.AuditLogs;
using Sgr.ExceptionHandling;
using Sgr.Utilities;

namespace Sgr.AspNetCore.Middlewares.ExceptionHandling
{
    /// <summary>
    /// 异常处理中间件 - 捕获并处理HTTP请求管道中的异常，将其转换为结构化的错误响应
    /// </summary>
    public class ExceptionHandlingMiddleware : IMiddleware
    {
        private readonly ILogger<ExceptionHandlingMiddleware> _logger;

        /// <summary>
        /// 可处理的返回类型列表
        /// </summary>
        /// <remarks>
        /// 这些类型是ASP.NET Core中常见的动作结果类型，当端点返回这些类型时，
        /// 中间件将处理发生的异常并返回标准化的错误响应。
        /// </remarks>
        public static List<Type> ObjectResultTypes { get; }

        static ExceptionHandlingMiddleware()
        {
            ObjectResultTypes = new List<Type>
            {
                typeof(void),
                typeof(JsonResult),
                typeof(ObjectResult),
                typeof(NoContentResult)
            };
        }

        /// <summary>
        /// 创建异常处理中间件的新实例
        /// </summary>
        /// <param name="logger">用于记录异常信息的日志记录器</param>
        public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        /// <summary>
        /// 处理HTTP请求并捕获异常
        /// </summary>
        /// <param name="context">当前HTTP上下文</param>
        /// <param name="next">请求处理管道中的下一个委托</param>
        /// <returns>表示异步操作的任务</returns>
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                // 如果响应已经开始发送，则无法修改状态码和响应内容
                if (context.Response.HasStarted)
                {
                    _logger.LogWarning("发生异常，但响应已经开始发送，无法处理异常！");
                    throw; // 重新抛出异常，让宿主处理
                }

                // 判断是否应该处理此异常
                if (ShouldHandleException(context))
                {
                    await HandleAndWrapExceptionAsync(context, ex);
                    return;
                }

                // 不处理此异常，重新抛出
                throw;
            }
        }

        /// <summary>
        /// 确定是否应该处理当前请求中的异常
        /// </summary>
        /// <param name="context">当前HTTP上下文</param>
        /// <returns>如果应该处理异常则为true，否则为false</returns>
        /// <remarks>
        /// 此方法根据请求的端点和返回类型来决定是否处理异常。
        /// 如果端点的返回类型在预定义的ObjectResultTypes列表中，或者不是IActionResult类型，
        /// 则会处理该异常。
        /// </remarks>
        protected virtual bool ShouldHandleException(HttpContext context)
        {
            // 获取当前请求的端点和返回类型
            var endpoint = context.GetEndpoint();
            var returnType = endpoint?.RequestDelegate?.Method?.ReturnType;

            if (returnType == null)
            {
                LogEndpointNotFound(context, endpoint);
                return false;
            }

            // 处理Task<T>类型的返回值
            returnType = TypeHelper.UnwrapTask(returnType);

            // 如果返回类型不是IActionResult，则处理异常
            if (!typeof(IActionResult).IsAssignableFrom(returnType))
            {
                return true;
            }

            // 检查是否是支持的IActionResult类型
            bool shouldHandle = ObjectResultTypes.Any(t => t.IsAssignableFrom(returnType));

            if (!shouldHandle)
            {
                LogUnsupportedReturnType(context, endpoint, returnType);
            }

            return shouldHandle;
        }

        /// <summary>
        /// 记录未找到端点或返回类型为空的警告日志
        /// </summary>
        private void LogEndpointNotFound(HttpContext context, Endpoint? endpoint)
        {
            _logger.LogWarning(
                "未处理的异常，原因：无法识别端点或返回类型为空。路径: {Path}, 方法: {Method}, Endpoint: {EndpointDisplayName}",
                context.Request.Path,
                context.Request.Method,
                endpoint?.DisplayName ?? "未知");
        }

        /// <summary>
        /// 记录不支持的返回类型的警告日志
        /// </summary>
        private void LogUnsupportedReturnType(HttpContext context, Endpoint? endpoint, Type returnType)
        {
            _logger.LogWarning(
                "未处理的异常，原因：返回类型 {ReturnType} 不在可处理列表中。Endpoint: {EndpointDisplayName}, 路径: {Path}, 方法: {Method}",
                returnType.Name,
                endpoint?.DisplayName,
                context.Request.Path,
                context.Request.Method);
        }

        /// <summary>
        /// 处理异常
        /// </summary>
        /// <param name="httpContext"></param>
        /// <param name="exception"></param>
        /// <returns></returns>
        protected async Task HandleAndWrapExceptionAsync(HttpContext httpContext, Exception exception)
        {
            //将错误信息记录至审计信息
            if (httpContext.Items[Constant.AUDITLOG_STATU_HTTPCONTEXT_KEY] is UserHttpRequestAuditInfo auditInfo)
            {
                auditInfo.Status = false;
                auditInfo.StatusMessage = exception.Message;
            }

            var converter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfo>();

            //输出错误日志
            string messageToLog = converter.TranslateToLog(exception);
            if (string.IsNullOrEmpty(messageToLog))
                _logger.LogError(exception,
                 "应用程序异常，请求路径: {Path}, 方法: {Method}",
                 httpContext.Request.Path,
                 httpContext.Request.Method);
            else
                _logger.LogError(
                    "应用程序异常，请求路径: {Path}, 方法: {Method}, \n错误详情: {ValidationErrors}",
                    httpContext.Request.Path,
                    httpContext.Request.Method,
                    messageToLog);

            //返回错误响应
            var errorInfo = converter.ConvertToProblemDetails(exception);
            errorInfo.Item2.Instance = httpContext.Request.Path;

            httpContext.Response.Clear();
            httpContext.Response.StatusCode = errorInfo.Item1;
            httpContext.Response.OnStarting(ClearCacheHeaders, httpContext.Response);
            httpContext.Response.Headers[Constant.SGR_ERRORHANDLE_HEADERNAME] = "true";
            httpContext.Response.Headers.ContentType = System.Net.Mime.MediaTypeNames.Application.Json;

            // 根据实际类型选择序列化
            var json = errorInfo.Item2 is ValidationProblemDetails validationProblem
                ? JsonHelper.SerializeObject(validationProblem)
                : JsonHelper.SerializeObject(errorInfo.Item2);

            await httpContext.Response.WriteAsync(json);
        }

        private Task ClearCacheHeaders(object state)
        {
            var response = (HttpResponse)state;

            response.Headers[HeaderNames.CacheControl] = "no-cache";
            response.Headers[HeaderNames.Pragma] = "no-cache";
            response.Headers[HeaderNames.Expires] = "-1";
            response.Headers.Remove(HeaderNames.ETag);

            return Task.CompletedTask;
        }
    }
}